package fudan.se.lab2.service;

import fudan.se.lab2.controller.request.DistributeRequest;
import fudan.se.lab2.domain.*;
import fudan.se.lab2.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Service
public class DistributeService {
    private final UserRepository userRepository;
    private final MeetingRepository meetingRepository;
    private final InvitationRepository invitationRepository;
    private final ContributionRepository contributionRepository;
    private final DistributionRepository distributionRepository;
    private final PostRepository postRepository;
    private final SecureRandom rand = new SecureRandom();
    private boolean flag;

    @Autowired
    public DistributeService(UserRepository userRepository, MeetingRepository meetingRepository, InvitationRepository invitationRepository, ContributionRepository contributionRepository, DistributionRepository distributionRepository, PostRepository postRepository) {
        this.userRepository = userRepository;
        this.meetingRepository = meetingRepository;
        this.invitationRepository = invitationRepository;
        this.contributionRepository = contributionRepository;
        this.distributionRepository = distributionRepository;
        this.postRepository = postRepository;
    }


    public boolean distribute(DistributeRequest request) {
        Meeting meeting = meetingRepository.findMeetingByFullName(request.getMeetingFullName());
        boolean status = false;
        if (meeting.getPcMembers().size() < 3)
            return false;
        if (request.getMethod().equals("relevance")) {
            status = distributeByRelevance(request.getMeetingFullName());
        } else if (request.getMethod().equals("amount")) {
            status = distributeByAmount(request.getMeetingFullName());
        }
        return status;
    }

    private boolean distributeByRelevance(String meetingFullName) {
        Meeting meeting = meetingRepository.findMeetingByFullName(meetingFullName);
        Iterable<Contribution> contributions = contributionRepository.findByMeetingFullName(meetingFullName);
        String chair = meeting.getChair();
        List<String> pcMembers = meeting.getPcMembers();
        List<String> allowedPcMembers;
        List<String> relevantPcMembers;
        List<String> pickedPcMembers;
        Distribution distribution;

        for (Contribution contribution : contributions) {
            allowedPcMembers = getAllowedPcMembers(pcMembers, contribution);

            if (allowedPcMembers.size() < 3)
                return false;

            relevantPcMembers = getRelevantPcMembers(allowedPcMembers, contribution, meetingFullName, chair);

            if (relevantPcMembers.size() >= 3) {
                pickedPcMembers = randomizeThreePcMembers(relevantPcMembers);
            } else {
                pickedPcMembers = randomizeThreePcMembers(allowedPcMembers);
            }

            for (String pickedPcMember : pickedPcMembers) {
                distribution = new Distribution(pickedPcMember, meetingFullName, contribution.getFileTitle(), 0);
                distributionRepository.save(distribution);
            }
        }
        return true;
    }

    private List<String> getAllowedPcMembers(List<String> pcMembers, Contribution contribution) {
        List<String> allowedPcMembers = new ArrayList<>();
        String contributor = contribution.getContributor();
        List<String> authorEmails = contribution.getAuthorEmails();
        Set<String> registeredAuthors = getRegisteredAuthors(authorEmails);

        for (String pcMember : pcMembers) {
            if (contributor.equals(pcMember) || registeredAuthors.contains(pcMember))
                continue;
            allowedPcMembers.add(pcMember);
        }
        return allowedPcMembers;
    }

    private Set<String> getRegisteredAuthors(List<String> authorEmails) {
        Set<String> registeredAuthors = new HashSet<>();
        User user;

        for (String authorEmail : authorEmails) {
            user = userRepository.findByEmail(authorEmail);
            if (user == null)
                continue;
            registeredAuthors.add(user.getUsername());
        }
        return registeredAuthors;
    }

    private List<String> getRelevantPcMembers(List<String> pcMembers, Contribution contribution, String meetingFullName, String chair) {
        List<String> relevantPcMembers;
        Set<String> selectedPcMembers = new HashSet<>();
        List<String> topics = contribution.getTopics();
        List<String> pcMemberTopics;
        Invitation invitation;

        selectedPcMembers.add(chair);

        for (String pcMember : pcMembers) {
            if (!pcMember.equals(chair)) {
                invitation = invitationRepository.findByToUsernameAndMeetingFullName(pcMember, meetingFullName);
                pcMemberTopics = invitation.getTopics();

                comparison:
                for (String pcMemberTopic : pcMemberTopics) {
                    for (String topic : topics) {
                        if (topic.equals(pcMemberTopic)) {
                            selectedPcMembers.add(pcMember);
                            break comparison;
                        }
                    }
                }
            }
        }

        relevantPcMembers = new ArrayList<>(selectedPcMembers);

        return relevantPcMembers;
    }

    private List<String> randomizeThreePcMembers(List<String> pcMembers) {
        List<String> threePcMembers = new ArrayList<>();
        if (pcMembers.size() == 3) {
            threePcMembers = pcMembers;
        } else {
            List<Integer> randomNumber = random(pcMembers.size() - 1);
            for (int i = 0; i < 3; i++) {
                threePcMembers.add(pcMembers.get(randomNumber.get(i)));
            }
        }
        return threePcMembers;
    }

    private List<Integer> random(int max) {
        List<Integer> random = new ArrayList<>();
        int size = 0;
        int number;
        while (size < 3) {
            number = this.rand.nextInt(max + 1);
            if (!random.contains(number)) {
                random.add(number);
                size++;
            }
        }
        return random;
    }

    private boolean distributeByAmount(String meetingFullName) {
        Meeting meeting = meetingRepository.findMeetingByFullName(meetingFullName);
        Iterable<Contribution> contributionIterable = contributionRepository.findByMeetingFullName(meetingFullName);
        List<String> pcMembers = meeting.getPcMembers();

        List<Contribution> contributions = new ArrayList<>();
        contributionIterable.forEach(contributions::add);

        int contributionSize = contributions.size();
        int pcMemberSize = pcMembers.size();

        int[] distribute = new int[contributionSize * 3];
        int[] count = new int[pcMemberSize];

        flag = false;

        dfs(contributions, pcMembers, distribute, count, 0);

        Distribution distribution;

        if (flag) {
            for (int i = 0; i < contributionSize; i++) {
                distribution = new Distribution(pcMembers.get(distribute[i]), meetingFullName, contributions.get(i).getFileTitle(), 0);
                distributionRepository.save(distribution);
                distribution = new Distribution(pcMembers.get(distribute[i + contributionSize]), meetingFullName, contributions.get(i).getFileTitle(), 0);
                distributionRepository.save(distribution);
                distribution = new Distribution(pcMembers.get(distribute[i + contributionSize * 2]), meetingFullName, contributions.get(i).getFileTitle(), 0);
                distributionRepository.save(distribution);
            }
            return true;
        } else
            return false;
    }

    private void dfs(List<Contribution> contributions, List<String> pcMembers, int[] distribute, int[] count, int p) {
        if (flag)
            return;
        int contributionSize = contributions.size();
        int pcMemberSize = pcMembers.size();
        int mx = countMax(count);
        int mn = countMin(count);

        if (mx - mn > contributionSize * 3 - p + 1)
            return;

        if (p == contributionSize * 3) {
            flag = checkBalance(mx, mn);
            return;
        }

        Contribution contribution = contributions.get(p % contributionSize);
        List<String> authorEmails = contribution.getAuthorEmails();
        Set<String> registeredAuthors = getRegisteredAuthors(authorEmails);

        for (int i = 0; i < pcMemberSize; i++) {
            if (contribution.getContributor().equals(pcMembers.get(i)) || registeredAuthors.contains(pcMembers.get(i)) || checkDuplicatePcMember(distribute, p, contributionSize, i))
                continue;
            distribute[p] = i;
            count[i] += 1;
            dfs(contributions, pcMembers, distribute, count, p + 1);
            if (flag)
                return;
            count[i] -= 1;
            distribute[p] = 0;
        }
    }

    private int countMax(int[] count) {
        int mx = -1;
        for (int i : count) {
            mx = Math.max(mx, i);
        }
        return mx;
    }

    private int countMin(int[] count) {
        int mn = 1 << 30;
        for (int i : count) {
            mn = Math.min(mn, i);
        }
        return mn;
    }

    private boolean checkBalance(int mx, int mn) {
        return mx - mn <= 1;
    }

    private boolean checkDuplicatePcMember(int[] distribute, int p, int contributionSize, int i) {
        if (p >= contributionSize && distribute[p - contributionSize] == i)
            return true;
        return (p >= contributionSize * 2 && distribute[p - contributionSize * 2] == i);
    }


    public List<Contribution> contributionToBeReviewed(String username) {
        List<Contribution> contributions = new ArrayList<>();
        Iterable<Distribution> it = distributionRepository.findByUsernameAndStatus(username, 0);
        Contribution contribution;
        for (Distribution distribution : it) {
            contribution = contributionRepository.findByMeetingFullNameAndFileTitle(distribution.getMeetingFullName(), distribution.getFileTitle());
            contributions.add(contribution);
        }
        System.out.println(contributions);
        return contributions;
    }

    public List<Contribution> contributionToBeCheckReviewedFirstly(String username) {
        List<Contribution> contributions = new ArrayList<>();
        Iterable<Distribution> it = distributionRepository.findByUsernameAndStatus(username, 1);
        Contribution contribution;
        Post post;
        for (Distribution distribution : it) {
            post = postRepository.findByMeetingFullNameAndTitleAndType(distribution.getMeetingFullName(), distribution.getFileTitle(), 1);
            if (post != null && post.getStatus() == 1) {
                contribution = contributionRepository.findByMeetingFullNameAndFileTitle(distribution.getMeetingFullName(), distribution.getFileTitle());
                contributions.add(contribution);
            }
        }
        return contributions;
    }

    public List<Contribution> contributionsWaitingForRebuttal(String username) {
        List<Contribution> contributions = new ArrayList<>();
        Iterable<Distribution> it = distributionRepository.findByUsernameAndStatus(username, 2);
        Contribution contribution;
        Post post;
        for (Distribution distribution : it) {
            post = postRepository.findByMeetingFullNameAndTitleAndType(distribution.getMeetingFullName(), distribution.getFileTitle(), 1);
            if (post != null && post.getStatus() == 0) {
                contribution = contributionRepository.findByMeetingFullNameAndFileTitle(distribution.getMeetingFullName(), distribution.getFileTitle());
                contributions.add(contribution);
            }
        }
        return contributions;
    }

    public List<Contribution> contributionsToBeCheckReviewedSecondly(String username) {
        List<Contribution> contributions = new ArrayList<>();
        Iterable<Distribution> it = distributionRepository.findByUsernameAndStatus(username, 2);
        Contribution contribution;
        Post post;
        for (Distribution distribution : it) {
            post = postRepository.findByMeetingFullNameAndTitleAndType(distribution.getMeetingFullName(), distribution.getFileTitle(), 2);
            if (post != null && post.getStatus() == 1) {
                contribution = contributionRepository.findByMeetingFullNameAndFileTitle(distribution.getMeetingFullName(), distribution.getFileTitle());
                contributions.add(contribution);
            }
        }
        return contributions;
    }

    public List<Distribution> allDistributionInThisConference(String meetingFullName) {
        List<Distribution> allDistributions = new ArrayList<>();
        Iterable<Distribution> distributions = distributionRepository.findByMeetingFullName(meetingFullName);
        for (Distribution distribution : distributions) {
            allDistributions.add(distribution);
        }
        System.out.println("alldis");
        return allDistributions;
    }
}
